route.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import { getServerSideConfig } from "@/app/config/server";
  2. import {
  3. ANTHROPIC_BASE_URL,
  4. Anthropic,
  5. ApiPath,
  6. DEFAULT_MODELS,
  7. ServiceProvider,
  8. ModelProvider,
  9. } from "@/app/constant";
  10. import { prettyObject } from "@/app/utils/format";
  11. import { NextRequest, NextResponse } from "next/server";
  12. import { auth } from "../../auth";
  13. import { isModelAvailableInServer } from "@/app/utils/model";
  14. const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
  15. async function handle(
  16. req: NextRequest,
  17. { params }: { params: { path: string[] } },
  18. ) {
  19. console.log("[Anthropic Route] params ", params);
  20. if (req.method === "OPTIONS") {
  21. return NextResponse.json({ body: "OK" }, { status: 200 });
  22. }
  23. const subpath = params.path.join("/");
  24. if (!ALLOWD_PATH.has(subpath)) {
  25. console.log("[Anthropic Route] forbidden path ", subpath);
  26. return NextResponse.json(
  27. {
  28. error: true,
  29. msg: "you are not allowed to request " + subpath,
  30. },
  31. {
  32. status: 403,
  33. },
  34. );
  35. }
  36. const authResult = auth(req, ModelProvider.Claude);
  37. if (authResult.error) {
  38. return NextResponse.json(authResult, {
  39. status: 401,
  40. });
  41. }
  42. try {
  43. const response = await request(req);
  44. return response;
  45. } catch (e) {
  46. console.error("[Anthropic] ", e);
  47. return NextResponse.json(prettyObject(e));
  48. }
  49. }
  50. export const GET = handle;
  51. export const POST = handle;
  52. export const runtime = "edge";
  53. export const preferredRegion = [
  54. "arn1",
  55. "bom1",
  56. "cdg1",
  57. "cle1",
  58. "cpt1",
  59. "dub1",
  60. "fra1",
  61. "gru1",
  62. "hnd1",
  63. "iad1",
  64. "icn1",
  65. "kix1",
  66. "lhr1",
  67. "pdx1",
  68. "sfo1",
  69. "sin1",
  70. "syd1",
  71. ];
  72. const serverConfig = getServerSideConfig();
  73. async function request(req: NextRequest) {
  74. const controller = new AbortController();
  75. let authHeaderName = "x-api-key";
  76. let authValue =
  77. req.headers.get(authHeaderName) ||
  78. req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() ||
  79. serverConfig.anthropicApiKey ||
  80. "";
  81. let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, "");
  82. let baseUrl =
  83. serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL;
  84. if (!baseUrl.startsWith("http")) {
  85. baseUrl = `https://${baseUrl}`;
  86. }
  87. if (baseUrl.endsWith("/")) {
  88. baseUrl = baseUrl.slice(0, -1);
  89. }
  90. console.log("[Proxy] ", path);
  91. console.log("[Base Url]", baseUrl);
  92. const timeoutId = setTimeout(
  93. () => {
  94. controller.abort();
  95. },
  96. 10 * 60 * 1000,
  97. );
  98. const fetchUrl = `${baseUrl}${path}`;
  99. const fetchOptions: RequestInit = {
  100. headers: {
  101. "Content-Type": "application/json",
  102. "Cache-Control": "no-store",
  103. [authHeaderName]: authValue,
  104. "anthropic-version":
  105. req.headers.get("anthropic-version") ||
  106. serverConfig.anthropicApiVersion ||
  107. Anthropic.Vision,
  108. },
  109. method: req.method,
  110. body: req.body,
  111. redirect: "manual",
  112. // @ts-ignore
  113. duplex: "half",
  114. signal: controller.signal,
  115. };
  116. // #1815 try to refuse some request to some models
  117. if (serverConfig.customModels && req.body) {
  118. try {
  119. const clonedBody = await req.text();
  120. fetchOptions.body = clonedBody;
  121. const jsonBody = JSON.parse(clonedBody) as { model?: string };
  122. // not undefined and is false
  123. if (
  124. isModelAvailableInServer(
  125. serverConfig.customModels,
  126. jsonBody?.model as string,
  127. ServiceProvider.Anthropic as string,
  128. )
  129. ) {
  130. return NextResponse.json(
  131. {
  132. error: true,
  133. message: `you are not allowed to use ${jsonBody?.model} model`,
  134. },
  135. {
  136. status: 403,
  137. },
  138. );
  139. }
  140. } catch (e) {
  141. console.error(`[Anthropic] filter`, e);
  142. }
  143. }
  144. console.log("[Anthropic request]", fetchOptions.headers, req.method);
  145. try {
  146. const res = await fetch(fetchUrl, fetchOptions);
  147. console.log(
  148. "[Anthropic response]",
  149. res.status,
  150. " ",
  151. res.headers,
  152. res.url,
  153. );
  154. // to prevent browser prompt for credentials
  155. const newHeaders = new Headers(res.headers);
  156. newHeaders.delete("www-authenticate");
  157. // to disable nginx buffering
  158. newHeaders.set("X-Accel-Buffering", "no");
  159. return new Response(res.body, {
  160. status: res.status,
  161. statusText: res.statusText,
  162. headers: newHeaders,
  163. });
  164. } finally {
  165. clearTimeout(timeoutId);
  166. }
  167. }